Migrate to TanStack Query for page ID fetching#157
Conversation
Co-Authored-By: Claude
b0b0920 to
75f072d
Compare
This comment was marked as outdated.
This comment was marked as outdated.
False positive. See https://tanstack.com/query/latest/docs/framework/react/devtools#install-and-import-the-devtools |
This comment was marked as outdated.
This comment was marked as outdated.
0c31581 to
ddf9961
Compare
This comment was marked as outdated.
This comment was marked as outdated.
- Configure QueryClient with 5min staleTime - Add ReactQueryDevtools for development - Wrap App in QueryClientProvider Co-Authored-By: Claude
- Add optional signal parameter to fetchPageId - Pass signal to fetch for request cancellation - Add tests for abort behavior Co-Authored-By: Claude
ddf9961 to
b94f125
Compare
Code Review: TanStack Query MigrationI've thoroughly reviewed PR #157 and this is an excellent migration that significantly improves the codebase. All tests are passing, the implementation is clean, and the documentation is comprehensive. Here's my detailed feedback: ✅ StrengthsArchitecture & Design
Code Quality
Performance
🔍 Minor Observations & Suggestions1. Bundle Size DocumentationLocation: ADR-004, line 42 Observation: This is documented in the ADR, which is good. For a feature like this, the bundle size increase is justified by the significant UX improvements (request cancellation, caching, deduplication). No action needed, but consider monitoring this in future PRs. 2. Query Key StructureLocation: queryKey: ['pageId', wikiUrl.href, pageTitle]Observation: Using 3. Empty Title HandlingLocation: queryFn: ({ signal }) => pageTitle ? fetchPageId(wikiUrl, pageTitle, signal) : null,Observation: This is the correct approach to avoid Suspense flash. The ternary inside 4. Error Boundary Reset PatternLocation: resetKeys={[debouncedTitle]}
onReset={reset}Observation: Excellent use of 5. Hidden Input in Error FallbackLocation: <input type="hidden" name="pageId" value="" data-testid="pageId-input" />Observation: Critical detail! This prevents form submission when page ID fetch fails. Without this, the form could submit with a missing 🎯 Suggestions (Optional Enhancements)1. Consider Adding Query Retry ConfigurationWhile you have // Optional: in PageIdFetcher.tsx
const { data: pageId } = useSuspenseQuery({
queryKey: ['pageId', wikiUrl.href, pageTitle],
queryFn: ({ signal }) => pageTitle ? fetchPageId(wikiUrl, pageTitle, signal) : null,
retry: (failureCount, error) => {
// Don't retry 404s
if (isNotFoundError(error)) return false;
return failureCount < 1;
},
});Rationale: 404s won't succeed on retry, so this could reduce unnecessary network requests. However, the current implementation is already correct with the global 2. Consider ReactQueryDevtools PositioningLocation: The <QueryClientProvider client={queryClient}>
{/* app content */}
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>The conditional ensures it's only included in dev builds (though Vite likely tree-shakes this anyway). 🔒 Security✅ No security concerns identified
📊 Performance Considerations✅ Performance improvements confirmed:
🧪 Test Coverage✅ Excellent test coverage:
📝 Documentation✅ Outstanding documentation:
Final VerdictStatus: ✅ APPROVED - Ready to merge This is a high-quality PR that:
The bundle size increase is justified by significant UX and DX improvements. No blocking issues found. Recommendation: Merge this PR. The suggested enhancements above are purely optional refinements, not blockers. Great work on this migration! The attention to detail (404 discrimination, hidden input in error fallback, resetKeys pattern) shows solid React expertise. 🎉 |
- Replace use() hook with useSuspenseQuery - Accept wikiUrl and pageTitle props instead of promise - Pass AbortSignal from query to fetchPageId - Update tests to use QueryClientProvider wrapper Co-Authored-By: Claude
b94f125 to
3a567c6
Compare
This comment was marked as outdated.
This comment was marked as outdated.
- Replace custom useDebounce with useDebouncedValue from @tanstack/react-pacer
- Remove useMemo promise creation pattern
- Use conditional rendering for PageIdFetcher (not enabled option)
- Add ErrorBoundary component for query errors
- Update all tests to use QueryClientProvider wrapper
Replace custom ErrorBoundary class with react-error-boundary library
and add QueryErrorResetBoundary to properly reset both React and
TanStack Query error states.
Key improvements:
- Install react-error-boundary (v6.1.0)
- Use QueryErrorResetBoundary + ErrorBoundary combination
- Use resetKeys={[debouncedTitle]} instead of key prop (no unmount)
- Distinguish 404 vs transient errors via error.cause.status
- 404 errors show "Page not found." (no retry)
- Transient errors show retry button with type="button"
- Add comprehensive tests for all error scenarios
Fixes the two-layer stuck state where neither React's error boundary
nor TanStack Query's cached error would reset on retry attempts.
Co-Authored-By: Claude
All 15 SearchForm tests were logging "No QueryClient set" errors to stderr because PageIdFetcher (rendered via PageTitleInput) uses useSuspenseQuery. Added QueryClientProvider wrapping to all test renders following the established pattern from PageIdFetcher and PageTitleInput tests. Co-Authored-By: Claude
Style the retry button in error messages as an inline text link instead of a block button, for better visual integration with the error message text. - Display inline with inherited font properties - Use primary color with underline (link-like appearance) - Remove block-level button styling for this context Co-Authored-By: Claude
Replaced by TanStack Pacer's useDebouncedValue Co-Authored-By: Claude
- Document migration rationale and trade-offs - Add TanStack Query patterns to CLAUDE.md - Include testing patterns and key benefits Co-Authored-By: Claude
3a567c6 to
feed0d0
Compare
Code Review SummaryThis PR implements a well-architected migration from the ✅ Strengths1. Architecture & Design
2. Error Handling
3. Test Coverage
4. Performance Optimizations
5. Code Quality
🔍 Observations & Suggestions1. Bundle Size Trade-offThe PR notes a +12KB gzipped increase (PR description) but ADR-004 says ~13.91KB. This discrepancy is minor but worth reconciling. Impact: For this use case, the bundle increase is justified given the benefits (caching, cancellation, DevTools). However, consider:
2. Retry Configuration InconsistencyIn retry: (failureCount, error) => !isNotFoundError(error) && failureCount < 1,This conflicts with the global Recommendation: This is correct behavior, but the documentation could be clearer:
3. Error Boundary Reset BehaviorThe
Current behavior is acceptable, but you might want to add a test case for "user types same title after error" to verify expected behavior. 4. Missing Test: Request CancellationWhile AbortSignal is tested in Recommendation: Consider adding a test in
5. DevTools in Production
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}This is a minor documentation improvement to make the intent explicit. 6. CSS Button Styling SpecificityThe new CSS rules ( Recommendation: Consider using a more specific class: .status-message-container .retry-button { /* ... */ }And add 🔒 Security Considerations✅ No security concerns identified:
📊 Performance Considerations✅ Performance improvements:
🧪 Test Coverage Analysis✅ Excellent coverage:
Potential additions (optional):
📝 Documentation Quality✅ Outstanding documentation:
Minor suggestion: Add a comment in 🎯 Final Recommendation✅ APPROVE - Ready to Merge This is a high-quality PR that:
The observations above are minor improvements that could be addressed in follow-up PRs if desired, but none are blockers. Suggested follow-ups (optional, not blocking):
Great work on this migration! 🎉 |
This app is a history reader, so such a freshy new page won't be a problem with a short revision history. |
Summary
Migrates page ID fetching from
useMemo+use()pattern to TanStack Query'suseSuspenseQuery, addressing the anti-pattern identified in React documentation.Closes #155
Changes
Core Migration
useSuspenseQuerywith query key['pageId', wikiUrl.href, pageTitle]useDebouncewith TanStack Pacer'suseDebouncedValue(300ms)AbortSignalparameter tofetchPageIdfor request cancellationQueryClientProviderwith 5min staleTime and ReactQueryDevtoolsCleanup
src/hooks/useDebounce.ts(replaced by TanStack Pacer)QueryClientProviderwrapperDocumentation
Key Benefits
✅ Request cancellation: In-flight requests automatically aborted on input change
✅ Automatic caching: Same page title returns cached result (5min staleTime)
✅ Deduplication: Multiple components requesting same data share one request
✅ Promise stability: Query cache handles promise reference stability
✅ DevTools: ReactQueryDevtools for debugging
Technical Details
Query Configuration
Empty Title Handling
Empty titles are handled inside
queryFnby returningnullimmediately (no Suspense flash).Testing Pattern
All components using queries must wrap in
QueryClientProvider:Trade-offs
Test Plan
Co-Authored-By: Claude
Editor: me